---
title: 代码实现（上）
description: 怎样创建领域对象、实现领域逻辑？
---

上一节我们根据 DDD 的分层架构，建立了程序的“骨架”，现在，我们来编写具体的逻辑，给骨架添上“血肉”。其实仅仅从功能完成的角度来说，这些程序很简单，<RedSpan>但关键是怎么按照 DDD 的要求来写。</RedSpan>


按照 DDD 的要求，首先要考虑的问题就是，怎么使代码和模型保持一致？这里可以细化成几个子问题，比如怎么实现业务规则、怎么表示对象间的关联、怎么区分领域逻辑和应用逻辑等等。


其次，我们还要考虑一些通用的编程问题，比如说是采用贫血模型还是充血模型、怎么减少重复、怎样实现模块化，等等。

## 面向对象和面向过程

提到编码，我们常常说起的一个问题就是：应该采用贫血模型还是充血模型？

贫血模型，英文叫做 Anemic Domain Model ，是 Martin Fowler 在 2003 年提出的，恰好在《DDD》这本书写作的同一年。贫血模型指的是**领域对象中只有数据，没有行为**，由于过于单薄，就好像人贫血了一样，显得不太健康。这种风格违背了面向对象的原则。


所以，Martin Fowler 认为这是一种反模式，他主张的方式叫做 Rich Domain Model，可以译作“富领域模型”，也就是**领域对象里既包含数据，也包含行为**。

在后面的讨论中，我会把贫血模型称为面向过程或者过程式编程，把富领域模型称为面向对象或对象式编程，因为真正的面向对象本来就是包含丰富逻辑的。

其实，早期的面向对象编程，主要是用来开发桌面软件的，比如说开发一个 Office、一个 IDE 等等。这类软件的特点是基本上整个软件的数据都能装入内存，这样就可以通过对象之间自由的导航实现复杂的逻辑。对象在内存里形成一种网状结构，称为对象图（Object Graph）。




但是企业应用则有一个本质的不同，就是数据主要在数据库里，每次只能把一小部分远程拿到内存，所以不能在内存里对对象进行自由地导航。这个区别就造成，<RedSpan>早期的面向对象编程很难直接用在企业应用，间接导致了贫血模型的普及</RedSpan>。尽管用 JPA 之类的 ORM 框架可以减少这种痛苦，但底层原因并没有消除，所以 ORM 框架在解决一些问题的同时，又带来了另外一些的问题。

早年，很多像 Martin Fowler 这样的专家认为面向对象就是王道，我把他们称为面向对象的“原教旨主义者”。但是到了今天，包括 Martin Fowler 在内的很多人已经成长为编程范式的“中立主义者”，也就是并不局限于面向对象，而是将面向对象、面向过程、面向方面、函数式等等编程范式结合起来。


<img src="https://wkq-img.oss-cn-chengdu.aliyuncs.com/20241202224739.png"/>


在纯粹的面向对象和纯粹的面向过程之间有一个广阔的“灰色地带”。这里面的变化非常多，难以穷尽。这两个极端都不是我们要追求的，我们要做的是找到其中的一个平衡点。

据目前国内多数人的编程习惯，我们采用这样的原则：<RedSpan>在领域对象不直接或间接访问数据库的前提下，尽量面向对象</RedSpan>。



为了理解这句话，<RedSpan>咱们一会儿先按面向过程的方式写出程序，然后通过重构，逐渐向面向对象靠拢</RedSpan>。在这个过程里，咱们可以体会不同编程范式的特点，以及思考它们怎样融合。

另外，由于不能在对象之间自由导航，所以相对传统的面向对象编程来说，我们的编程风格会<RedSpan>偏过程</RedSpan>一些。

## 开卡和验卡

好，理论就讲这么多，回到开发过程。现在咱们俩都是资深程序员，另外，还有一个领域专家和我们配合，就叫他老王。

假设距离领域建模已经过去三四天了，今天我们开始真正编写“添加组织”这个功能。但是有两个问题：第一，尽管几天前已经澄清过需求，但会不会当时有什么遗漏呢？第二，就算当时没有遗漏，那么最近几天，会不会又有些新的变化没有通知到我们呢？

所以，有必要在真正动手编码之前，找老王再次确认一下。这实际上是敏捷中的一个常用实践，叫做“<RedSpan>开卡</RedSpan>”。相应地，在代码开发完毕，正式提交测试之前，也会把领域专家或测试人员叫过来大体看一下有没有方向上的错误，这一步叫“<RedSpan>验卡</RedSpan>”。这两个实践可以有效避免因为需求理解不一致而导致的返工。

经过和老王确认，我们果然又发现了几个新的业务规则。比如，同一个组织里，不能有两个同名的下级组织。也就是说，假如“金融开发中心”下面已经有“开发一组”了，那么新加的开发组，不能也叫“开发一组”。

下面这张表，是和老王澄清需求以后整理的和“添加组织”有关的业务规则。其中绿色部分是这次新加的：

<img src="https://wkq-img.oss-cn-chengdu.aliyuncs.com/20241202225141.png"/>

其实，和业务人员不断澄清需求，不断补充新的领域知识，正是 DDD 和敏捷软件开发的常态。


在这个表里我们发现，有一些“租户必须有效”“上级组织必须有效”这样的规则，说明在相应的实体里要增加状态属性来表达“有效”“终止”等状态。经过和老王的讨论，我们决定在租户、组织、组织类别、员工、客户、合同、项目等实体中都增加状态，并且在数据库里添加相应的字段。


DDD 强调，<RedSpan>在代码编写阶段，如果发现模型的问题，要及时修改模型，始终保持代码和模型的一致</RedSpan>。

## 面向过程的代码
需求澄清以后，我们终于可以写代码了。假如我刚学了一点 DDD 的分层架构，但是只会面向过程的方法，那么写出来大概是下面这个样子。


我们先通过包结构，看一下总体的逻辑。
<img src="https://wkq-img.oss-cn-chengdu.aliyuncs.com/20241202225849.png"/>


这里分成了六步
* 第一步,适配器层里的 OrgController 通过 Restful API 接收到 <RedSpan>添加组织</RedSpan> 的请求，请求数据封装在 OrgDto 里。
* 第二步，Controller 以 OrgDto 为参数，调用应用层里的 OrgService 服务中的 addOrg() 方法进行处理。
* 第三步，OrgService 对参数进行校验，过程中会调用适配器层里的 Repository 来访问数据库。
* 第四步，OrgService 创建领域层里的 Org 对象，也就是组织对象。
* 第五步，OrgService 调用 OrgRepository 把 <RedSpan>组织对象</RedSpan> 存储到数据库，并回填组织 id。
* 第六步，OrgService 把组织对象装配成 DTO，返回给控制器，控制器再返回给前端。

目前主要逻辑都集中在 OrgService 里，也就是第三步到第六步。

我们再看看两个主要的数据类。一个是领域对象类 Org。
```java
/**
 * <h2> 领域对象类 Org</h2>
 */
public class Org {
    private Long id;
    private Long tenantId;
    private Long superiorId;
    private String orgTypeCode;
    private Long leaderId;
    private String name;
    private OrgStatus status;
    private LocalDateTime createdAt;
    private LocalDateTime lastUpdateAt;
    private Long createdBy;
    private Long lastUpdateBy;

    public Org() {
        this.status = OrgStatus.EFFECTIVE;// 组织的初始状态默认为有效
    }
}

```

另一个是 OrgDto。

```java
public class OrgDto {
    private Long id;
    private Long tenantId;
    private Long superiorId;
    private String orgTypeCode;
    private Long leaderId;
    private String name;
    private String status;
    private LocalDateTime createdAt;
    private Long createdBy;
    private LocalDateTime lastUpdatedAt;
    private Long lastUpdatedBy;

    // getters and setters ...
}
```

目前这两个类十分相似，而且都只有数据没有行为。唯一的不同是，Org 的状态属性 Status 是一个枚举类型，算是比 OrgDto 略微“面向对象”一点。

## 层间依赖原则和依赖倒置

现在我们从 Controller 开始逐层往下捋，下面是 Controller 的代码。
```java
@RequestMapping("/api/organizations")
@RestController
public class OrgController {
    private final OrgService orgService;
    @Autowired
    public OrgController(OrgService orgService) {
        this.orgService = orgService;
    }
    /**
     * 添加组织
     */
    @PostMapping
    public OrgDto add(@RequestBody OrgDto request) {
        // 从请求里解析出 userId
        Long userId = 1L;
        return orgService.addOrg(request, userId);
    }
}
```

简单起见，我省略了身份认证、Http 返回码等处理。所以 addOrg 方法中只有一句，就是调用 Service。然而就是这一句，已经<RedSpan>违反了层间依赖原则</RedSpan>。你能发现是哪里出了问题吗？

没错，由于 OrgDto 是应用服务中 addOrg 方法的入口参数类型，所以应用层是依赖 OrgDto 的，而 OrgDto 又在适配器层。也就是说，应用层依赖了适配器层。应用层在适配器层的内层，而内层是不应该依赖外层的。这样，就违反了层间依赖原则。

改起来倒是简单，只要把 OrgDto 移动到应用层就可以了，修改后是后面这样。

<img src="https://wkq-img.oss-cn-chengdu.aliyuncs.com/20241202231853.png"/>


看完 Controller，我们再重点看一下应用层的 OrgService，代码结构是后面这样。


```java
@Service
public class OrgService {
    private final UserRepository userRepository;
    private final TenantRepository tenantRepository;
    private final OrgTypeRepository orgTypeRepository;
    private final OrgRepository orgRepository;
    private final EmpRepository empRepository;

    @Autowired
    public OrgService(UserRepository userRepository
            , TenantRepository tenantRepository
            , OrgRepository orgRepository
            , EmpRepository empRepository
            , OrgTypeRepository orgTypeRepository) {

            //为各个Repository赋值...
    }

    public OrgDto addOrg(OrgDto request, Long userId) {
        validate(request, userId);
        Org org = buildOrg(request, userId);
        org = orgRepository.save(org);
        return buildOrgDto(org);
    }

    private OrgDto buildOrgDto(Org org) {
        // 将领域对象的值赋给DTO...
    }

    private Org buildOrg(OrgDto request, Long useId) {
        // 将DTO的值赋给领域对象...
    }

    private void validate(OrgDto request) {
        //进行各种业务规则的校验，会用到上面的各个Repository...
    }
}
```
主控逻辑在 addOrg 方法里（19～24 行），也很简单，先校验参数，再创建领域对象，然后保存到数据库，最后返回 DTO。不过在这里，是不是又发现了一处违反层间依赖的地方呢？

就是对仓库，也就是 Repository 的调用。仓库放在适配器层，而应用层调用了仓库，造成应用层对适配器层的依赖，这就再一次违反了层间依赖规则。

这里就要用到一个技巧了，通过两步就可以解决.
* 第一步，从仓库抽出一个接口，原来的仓库成为了这个接口的实现类。
* 第二步，把这个接口移动到领域层。

修改后的结构是后面这样。

<img src="https://wkq-img.oss-cn-chengdu.aliyuncs.com/20241202232458.png"/>

仓库接口都按照 XxxRepository 的形式命名。而仓库的实现是在接口名字的后面加上 Jdbc ，这是因为在目前的例子里只是用了 Jdbc 来做持久化。OrgService 中的代码，除了 import 语句要调整一下以外，其他都没有变化。Spring 框架会“偷偷地”把实现类注入到相关属性。

以 OrgRepository 为例，接口代码是后面这样的。
```java
package domain.orgmng; //注意：仓库的接口在领域层

//import ...

public interface OrgRepository {
    Optional<Org> findByIdAndStatus(long tenantId, Long id
                                    , OrgStatus status);
    int countBySuperiorAndName(long tenantId, Long superiorId
                                    , String name);
    boolean existsBySuperiorAndName(Long tenant, Long superior
                                    , String name);
    Org save(Org org);
}
```
而实现类仍然在适配器层，像后面这样。
```java
//注意：Repository 的实现类在适配器层
package adapter.driven.persistence.orgmng;

//import ...

@Repository
public class OrgRepositoryJdbc implements OrgRepository {
    JdbcTemplate jdbc;
    SimpleJdbcInsert insertOrg;

    @Autowired
    public OrgRepositoryJdbc(JdbcTemplate jdbc) {
        //...
    }

    @Override
    public Optional<Org> findByIdAndStatus(Long tenantId, Long id, OrgStatus status) {
        //...
    }

    @Override
    public int countBySuperiorAndName(Long tenantId, Long superiorId, String name) {
        //...
    }

    @Override
    public boolean existsBySuperiorAndName(Long tenant, Long superior, String name) {
        //...
    }

    @Override
    public Org save(Org org) {
        //...
    }
}
```


为什么这样做就解决了层间依赖呢？让我们画一个 UML 图来更直观地看一下。

<img src="https://wkq-img.oss-cn-chengdu.aliyuncs.com/20241202232707.png"/>

这是和程序结构等价的 UML 图。不过要注意，我们现在画的是设计模型的类图，和前面领域建模的类图会有一些区别。

首先，在设计图里用的是英文，而领域模型图里用了中文。这是因为用中文更容易和领域专家交流，而设计图是给程序员看的，用英文更贴近代码。中英文的转换则依照词汇表。

第二个区别是，这个图里画的都不是领域对象，而是用来实现程序的对象。当然，设计图里也可以画领域对象，只不过针对目前这个问题，我们把领域对象省略了。但是反过来，领域模型图中一定不能存在只有技术人员才懂的内容。

现在我们看一下从 OrgService 到领域层中仓库接口的三个箭头。它们实际上代表了 OrgService 中用这三个接口定义的属性。这些箭头也是关联关系，只不过是技术意义上的。由于 OrgService 可以通过属性导航到仓库，而仓库中并没有属性能够导航到 OrgService，所以关联是单向的。关联也是一种依赖关系，所以可以说 OrgServie 依赖仓库，说明应用层依赖领域层。

再看从仓库实现类指向接口的带虚线的空三角箭头。这个符号表示类对接口的实现关系，在领域模型中一般是不使用的。实现关系也是一种依赖，所以现在适配器层也依赖了领域层。

原来应用层对适配器层的依赖神奇地消失了，取而代之的是应用层和适配器层两者对领域层的依赖。层间依赖的问题就解决了。

原来是别的层依赖适配器，现在通过抽取和移动接口，变成了适配器依赖别的层，依赖关系被“倒过来”了。所以这种技巧就称为**依赖倒置**（dependency inversion），<RedSpan>是面向对象设计中常见的调整依赖关系的手段</RedSpan>。


为什么要把仓库接口移动到领域层而不是应用层呢？移到应用层不是也可以解决这个问题吗？

就目前而言，移动到应用层也可以。但是领域层中的代码也有可能访问数据库。所以要移动到领域层，否则会出现新的层间依赖问题。



## 总结
编写代码，要实现两个要求：一个是保持代码和模型一致；另一个是符合通用的编程原则和最佳实践。

敏捷中“开卡”和“验卡”两个实践，补充了业务规则。

**编写代码的时候如果发现模型的问题，要及时修改模型，始终保持代码和模型的一致**。

在代码层面，重点是解决层间依赖问题。我们做了两个改进：一是把 DTO 从适配器层移到了应用层；另一个是采用“依赖倒置”原则，使适配器层依赖于领域层。

